package bancosys.tec.rtm.exception;

import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.hibernate.validator.InvalidStateException;

/**
 * ExceptionMonitor
 * 
 * @author Koji
 */
public class ExceptionMonitor implements ExceptionHandler {
    private static Logger log = Logger.getLogger(ExceptionMonitor.class);

    /**
     * Controle do armazenamento das exceções.
     */
    private ExceptionPersister exceptionPersister;

    private short diasArmazenarLimpezaRTM;

    /**
     * Busca de exceções armazenadas.
     */
    private ExceptionFinder exceptionFinder;

    /**
     * ExceptionHandler (sem uso atualmente)
     */
    private List<ExceptionHandler> exceptionHandlers = new ArrayList<ExceptionHandler>();

    /**
     * Mecanismo secundário de armazenamento das exceções.
     */
    private BackupExceptionPersister backupExceptionPersister = new FileBackupExceptionPersister();

    /**
     * Adiciona um ExceptionHandler a lista atual
     * 
     * @param exceptionHandler {@link ExceptionHandler}
     */
    public void addHandler(ExceptionHandler exceptionHandler) {
        this.exceptionHandlers.add(exceptionHandler);
    }

    /**
     * Atribui o objeto de persistência dos erros. Persister possui apenas metodo set para que não seja chamado o persist sem controle de
     * backup
     * 
     * @param persister {@link ExceptionPersister}
     */
    public void setPersister(ExceptionPersister persister) {
        this.exceptionPersister = persister;
    }

    /**
     * Atribui o objeto de busca de erros.
     * 
     * @param finder {@link ExceptionFinder}
     */
    public void setFinder(ExceptionFinder finder) {
        this.exceptionFinder = finder;
    }

    /**
     * Define o Exception persister de backup.
     * 
     * @param backupExceptionPersister o eception persister de backup.
     */
    public void setBackupExceptionPersister(BackupExceptionPersister backupExceptionPersister) {
        this.backupExceptionPersister = backupExceptionPersister;
    }

    /**
     * @return exceptionFinder
     */
    public ExceptionFinder getFinder() {
        return this.exceptionFinder;
    }

    /**
     * @param exception {@link Throwable}
     */
    public void monitor(final Throwable exception) {
        log.info(exception.getMessage(), exception);
        try {
            if (this.exceptionPersister == null) {
                throw new NullPointerException("Nenhuma persistência definida para o RTM.");
            }

            // chamada ao metodo de persistencia
            if (exception instanceof InvalidStateException) {
                this.exceptionPersister.persist(new ContextualizedInvalidStateException((InvalidStateException) exception));
            } else {
                this.exceptionPersister.persist(exception);
            }
            
            if (exception.getCause() != null && !exception.equals(exception.getCause())) {
                // Verifica excecoes exibidas apenas atraves do java.sql.SQLException.getNextException()
                Throwable nextException = seekNextException(exception.getCause());
                if (nextException != null) {
                    this.monitor(nextException);
                }
            }

            try {
                this.loadAndSaveBackup();
            } catch (Throwable e) {
                log.error("Erro recuperando backup do RTM.", e);
            }
        } catch (Throwable e) {
            log.error("Erro monitorando exceção pelo RTM, enviando para backup.", e);
            this.storeBackup(exception);
        }
    }

    /**
     * Busca recursivamente excecoes acessiveis apenas pelo metodo java.sql.SQLException.getNextException()
     * @param exception {@link Throwable}
     * @return nextException {@link Throwable}
     */
    private Throwable seekNextException(Throwable exception) {
		if (exception instanceof SQLException) {
			return ((SQLException) exception).getNextException(); 
		} else if (exception.getCause() != null && !exception.equals(exception.getCause())) {
			return seekNextException(exception.getCause());
		}
		return null;
	}

	/**
     * Método para armazenar o erro caso o persister falhe
     * 
     * @param exception {@link Throwable}
     */
    private synchronized void storeBackup(Throwable exception) {
        try {
            this.backupExceptionPersister.store(exception);
        } catch (Throwable t) {
            log.error("Erro executando backup de exceção.", t);
        }
    }

    /**
     * Método chamado para carregar e salvar os registros do backup do persister.
     * 
     * @throws Throwable e
     */
    public synchronized void loadAndSaveBackup() throws Throwable {
        if (!this.backupExceptionPersister.isEmpty()) {
            try {
                log.debug("Backup encontrado, tentando restaurar...");
                this.backupExceptionPersister.loadAndSaveBackupInExceptionPersister(this.exceptionPersister);
                log.debug("Backup restaurado com sucesso.");
                this.backupExceptionPersister.clearBackup();
            } catch (IOException e) {
                log.error("O Backup não pode ser restaurado.", e);
            } catch (ClassNotFoundException e) {
                log.error("O Backup não pode ser restaurado.", e);
            }
        }
    }

    /**
     * Faz a limpeza do RTM.
     * 
     * @param exception
     */
    public void cleanRTM() {
        try {
            this.exceptionPersister.cleanRTM(this.diasArmazenarLimpezaRTM);
        } catch (RTMException e) {
            log.error("Erro ao realizar limpeza do RTM.");
        }
    }

    /**
     * @return short
     */
    public short getDiasArmazenarLimpezaRTM() {
        return this.diasArmazenarLimpezaRTM;
    }

    /**
     * @param diasArmazenarLimpezaRTM short
     */
    public void setDiasArmazenarLimpezaRTM(short diasArmazenarLimpezaRTM) {
        this.diasArmazenarLimpezaRTM = diasArmazenarLimpezaRTM;
    }
}
